עבודה עם STL )Dmitry Korolev יניב סבו )מבוסס על המאמר של
הקדמה STL = Standard Template Library הספרייה הסטנדטית של ++C. כוללת את רוב האלגוריתמים ומבני הנתונים הבסיסיים במדעי המחשב. Heavily parameterized כמעט כל רכיב ב STL הוא.template
Containers STL כולל :containers classes מחלקות שתפקידן להכיל אובייקטים אחרים. לדוגמה, STL כולל את המחלקות: deque, vector, list, set, multiset, map, multimap,
לפני שנתחיל כשתוכנית משתמשת ב,STL היא צריכה לכלול את ה headers המתאימית. #include <stack> כל ה- STL מרוכז ב.namespace std using namespace std; כשיוצרים container יש לספק גם את סוג הנתונים שהוא יכיל. vector<int> N; vector< vector<int> > CorrectDefinition; vector<vector<int>> WrongDefinition; // Wrong: compiler may be confused by 'operator >>'
Vector vector<int> v(10); for(int i = 0; i < 10; i++) { v[i] = (i+1)*(i+1); vector<int> v; vector<int> v[10]; וקטור הוא למעשה מערך עם פונקציות נוספות. :C לקוד backward-compatible מה שתי השורות הבאות יעשו? וקטור יכול לדווח על הגודל שלו: int elements_count = v.size(); יש לשים לב כי size() הוא!unsigned
- המשך Vector איך בודקים האם וקטור ריק? bool is_nonempty_notgood = (v.size() >= 0); // Try to avoid this bool is_nonempty_ok =!v.empty(); vector<int> v; for(int i = 1; i < 1000000; i *= 2) { v.push_back(i); :vector vector<int> v(20); for(int i = 0; i < 20; i++) { v[i] = i+1; v.resize(25); for(int i = 20; i < 25; i++) { v.push_back(i*2); // Writes to elements with indices [25..30), not [20..25)! < הכנסת איבר חדש ל פונקציית :resize
- המשך Vector שימוש בהרבה push_back עלול לגרום להקצאות זכרון מיותרות. לדוגמה, עבור וקטור שבו 1000 איברים והגודל המוקצה שלו הוא 1024, אם נוסיף 50 איברים באמצעות push_back נקבל וקטור שהגודל המוקצה שלו הוא.2048 במקום, ניתן להשתמש ב.vector.reserve(1050)
- המשך Vector פונקציית vector.clear() אתחול וקטור: יצירת מערך דו מימדי: - מרוקנת את הוקטור. vector<int> v1; //... vector<int> v2 = v1; vector<int> v3(v1); vector<int> Data(1000); // 1000 zeros after creation vector<string> names(20, Unknown ); vector< vector<int> > Matrix; int N, M; vector< vector<int> > Matrix(N, vector<int>(m, -1)); אתחול מטריצה:
- המשך Vector העברת וקטור כפרמטר לפונקציה: void some_function(vector<int> v) { // Never do it unless you re sure what you do! //... void some_function(const vector<int>& v) { // OK //... int modify_vector(vector<int>& v) { // Correct V[0]++;
Pairs הוא טיפוס המכיל אובייקט אחד מסוג T1 pair<t1, <T2 ואובייקט שני מסוג T2. לדוגמה: pair<string, pair<int,int> > P; string s = P.first; // extract string int x = P.second.first; // extract first int int y = P.second.second; // extract second int היתרון הגדול של pairs הוא שיש להם פונקציית השוואה built-in שמבצעת השוואה קודם לפי המפתח הראשון ואם הם שווים מבצעת השוואה לפי המפתח השני.
Iterators איטרטורים הם המנגנון המאפשר לגשת למידע הנמצא ב containers ומהווים בעצם הכללה של מצביעים. ממומשים בצורה גנרית. איטרטורים רגילים iterators) (normal מאפשרים: לקבל את הערך שמוצבע ע"י האיטרטור: int x = *it להגדיל ולהקטין איטרטורים: it-- it++, להשוות איטרטורים: ==,=! איטרטורים מסוג בנוסף: random access iterators מאפשרים הוספה/החסרה לאיטרטור: it+=20 שקול לביצוע איברים קדימה..int n = it1-it2 קבלת המרחק בין איטרטורים: 20 shift
- המשך Iterators :container לדוגמה הפיכת template<typename T> void reverse_array(t *first, T *last) { if(first!= last) { while(true) { swap(*first, *last); first++; if(first == last) { break; last--; if(first == last) { break; end האלגוריתמים ב STL משתמשים בשני :iterators begin, end כאשר begin מצביע לאיבר הראשון ו מצביע לאיבר האחד אחרי אחרון. לכל container יש שתי פונקציות: end() begin(), שמחזירות את האיטרטורים המתאימים.
- המשך Iterators.c.begin() == c.end() c לכן, ניתן לומר כי ריק אמ"מ עבור containers שהאיטרטורים שלהם הם random.c.end()-c.begin()=c.size() מתקיים: access iterators STL היא: לכן הפונקציה להפיכת container עבור template<typename T> void reverse_array_stl_compliant(t *begin, T *end) { // We should at first decrement 'end' // But only for non-empty range if(begin!= end) { end--; if(begin!= end) { while(true) { swap(*begin, *end); begin++; If(begin == end) { break; end--; if(begin == end) { break;
שימוש ב iterators כל אובייקט עם מספיק פונקציונאליות יכול להיות מועבר לאלגוריתמים ופונקציות של STL )לדוגמה מצביע(. לדוגמה: vector<int> v; //... vector<int> v2(v); vector<int> v3(v.begin(), v.end()); // v3 equals to v2 int data[] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 ; vector<int> primes(data, data+(sizeof(data) / sizeof(data[0]))); // not recommended vector<int> v; //... vector<int> v2(v.begin(), v.begin() + (v.size()/2)); int data[10] = { 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 ; reverse(data+2, data+6); // the range { 5, 7, 9, 11 is now { 11, 9, 7, 5 ;
- שימוש ב iterators המשך לכל container יש גם את הפונקציות rend() rbegin(), שמחזירות.reverse iterators vector<int> v; vector<int> v2(v.rbegin()+(v.size()/2), v.rend()); כדי ליצור איטרטור עלינו לציין את הטיפוס שעליו הוא מצביע. ניתן לייצר איטרטור עבור container באמצעות הוספת ::reverse_iterator, ::iterator, ::const_iterator,.container לשם ה ::const_reverse_iterator vector<int> v; //... // Traverse all container, from begin() to end() for(vector<int>::iterator it = v.begin(); it!= v.end(); it++) { *it++; // Increment the value iterator is pointing to
אלגוריתמים STL כולל גם אלגוריתמים שניתן להפעיל על איטרטורים. אלגוריתם reverse שראינו. אלגוריתם - find מקבל שני איטרטורים וערך לחיפוש. מחזיר את האיטרטור שמצביע להופעה הראשונה של הערך אם נמצא בין האיטרטורים ואחרת את האיטרטור הסופי )כלומר הימני מבין שני האיטרטורים(. כדי לקבל את אינדקס האיבר שנמצא יש לחסר את האיטרטור של ההתחלה מהתוצאה של.find vector<int> v; if(find(v.begin(), v.end(), 49)!= v.end()) { //...
אלגוריתמים - המשך - min_element, max_element מחזירים את האיטרטור לאיבר המתאים )מקסימלי/מינימלי(. int data[5] = { 1, 5, 2, 4, 3 ; vector<int> X(data, data+5); int v1 = *max_element(x.begin(), X.end()); // Returns value of max element in vector int i1 = min_element(x.begin(), X.end()) X.begin; // Returns index of min element in vector int v2 = *max_element(data, data+5); // Returns value of max element in array int i3 = min_element(data, data+5) data; // Returns index of min element in array - ממיין את האיברים שבין שני האיטרטורים: vector<int> X; Int A[10]; //... sort(x.begin(), X.end()); // Sort array in ascending order sort(a, A+10); // Sort A in ascending order sort(x.rbegin(), X.rend()); // Sort array in descending order using with reverse iterators sort
אלגוריתמים - המשך מה נעשה אם נרצה למיין טיפוסים לא פרמיטיביים? sort() מבוסס על אותה טכניקה שמופיעה בכל המקומות הרלוונטיים ב- STL : השוואות מתבצעות באמצעות אופרטור >. struct fraction { int n, d; // (n/d) bool operator < (const fraction& f) const { return n*f.d < f.n*d; ; vector<fraction> v; sort(all(v));
אלגוריתמים - המשך מה נעשה אם נרצה למיין במספר צורות את אותו טיפוס? typedef pair<double, double> dd; const double epsilon = 1e-6; struct sort_by_polar_angle { dd center; template<typename T> sort_by_polar_angle(t b, T e) { //. bool operator () (const dd& a, const dd& b) const { double p1 = atan2(a.second-center.second, a.first-center.first); double p2 = atan2(b.second-center.second, b.first-center.first); return p1 + epsilon < p2; ; vector<dd> points; sort(points.begin(), points.end(), sort_by_polar_angle(points.begin(), points.end()));
אלגוריתמים - המשך אלגוריתמים נוספים: b).max(a, b), min(a, b), swap(a, אלגוריתמים שימושיים נוספים הם next_permutation, next_permutation(begin, end).prev_permutation מכניס לתחום end) (begin, את הפרומטציה הבאה של אותם איברים, או מחזירה false אם זו האחרונה. vector<int> v; for(int i = 0; i < 10; i++) { v.push_back(i); do { Solve(..., v); while(next_permutation(v.begin(), v.end());
אלגוריתמים - המשך אלגוריתם to_begin) copy(from_begin, from_end, מעתיק איברים מהתחום הראשון לשני. בתחום השני צריך להיות מספיק מקום. vector<int> v1; vector<int> v2; //... // Now copy v2 to the end of v1 v1.resize(v1.size() + v2.size()); // Ensure v1 have enough space copy(v2.begin(), v2.end(), v1.end() - v2.size()); // Copy v2 elements right after v1 ones
הערות void f(const vector<int>& v) { הקוד הבא ייצור שגיאה: for( vector<int>::iterator it = v.begin(); // hm... where s the error?.. //... הקוד הנכון נראה כך: void f(const vector<int>& v) { int r = 0; for(vector<int>::const_iterator it = v.begin(); it!= v.end(); it++) //
הערות - המשך #define sz(a) int((a).size()) #defile all(c) (c).begin(),(c).end() #define tr(c,i) for(typeof((c).begin()) i = (c).begin(); i!= (c).end(); i++) #define present(c,x) ((c).find(x)!= (c).end()) #define cpresent(c,x) (find(all(c),x)!= (c).end()) void f(const vector<int>& v) { int r = 0; tr(v, it) { r += (*it)*(*it); return r; מאקרויים שימושיים )?(: המאקרו tr מאפשר לבצע איטרציה על איברי כל container בקלות. לדוגמה:
שינוי המידע ב Vector vector<int> v; //... :insert() v.insert(1, 42); // Insert value 42 after the first iterator ניתן להכניס איבר לוקטור באמצעות יש לשים לב כי insert של vector גורם להזזת איברים ולכן מומלץ להכניס מספר איברים בפקודה אחת: vector<int> v; vector<int> v2; //.. // Shift all elements from second to last to the appropriate number of elements. // Then copy the contents of v2 into v. v.insert(1, all(v2)); erase(iterator); erase(begin iterator, end iterator); לוקטור יש גם פונקציית :erase
String.strings יש ל STL container מיוחד לעבודה עם string של STL מאפשר מספר פעולות חשובות כמו שרשור,)s1+s2( קלט של מחרוזת )s )cin << ופעולות רבות נוספות. string s = "hello"; string s1 = s.substr(0, 3), // "hel" s2 = s.substr(1, 3), // "ell" s3 = s.substr(0, s.length()-1), // "hell" s4 = s.substr(1); // "ello" מה יקרה כאשר = 0 s.length()?
Set עץ אדום שחור מאפשר הוספה, מחיקה וחיפוש של איברים ב.O(logN) העץ לא יכול להכיל איברים כפולים. מספר האיברים ב set מוחזר בסיבוכיות (1)O. set<int> s; for(int i = 1; i <= 100; i++) { s.insert(i); // Insert 100 elements, [1..100] s.insert(42); // does nothing, 42 already exists in set for(int i = 2; i <= 100; i += 2) { s.erase(i); // Erase even values int n = int(s.size()); // n will be 50.set כמובן שלא ניתן לבצע push_back() על
- המשך Set מעבר על האיברים בסדר עולה: // Calculate the sum of elements in set set<int> S; //... int r = 0; for(set<int>::const_iterator it = S.begin(); it!= S.end(); it++) { r += *it; ניתן גם להשתמש במאקרו שהגדרנו: set< pair<string, pair< int, vector<int> > > SS; int total = 0; tr(ss, it) { total += it->second.first;
- המשך Set אסור להשתמש באלגוריתם find של STL כדי לחפש איבר ב - set זה יתבצע ב.O(n) במקום זאת יש לקרוא ל.set::find set::find מחזיר איטרטור לאיבר שנמצא, או end() אם לא נמצא. set<int> s; //... if(s.find(42)!= s.end()) { // 42 presents in set else { // 42 not presents in set
- המשך Set.erase כדי למחוק איבר מה set משתמשים ב set<int> s; // s.insert(54); s.erase(29); set<int> s; // s.insert(54); s.erase(29); set יכול גם לקבל מערך ב.constructor ניתן להשתמש בזה כדי להסיר כפילויות ולמיין במהירות: vector<int> v; // set<int> s(all(v)); vector<int> v2(all(s));
Map דומה מאוד ל,set אך הוא מכיל זוגות value>.pair<key, יכול להיות לכל היותר pair אחד עם אותו.key ב map<string, int> M; M["Top"] = 1; M["Coder"] = 2; M["SRM"] = 10; int x = M["Top"] + M["Coder"]; if(m.find("srm")!= M.end()) { M.erase(M.find("SRM")); // or even M.erase("SRM") map<string, int> M; // int r = 0; tr(m, it) { r += it->second; :keys.map map map map האופרטור [] מוגדר על ניתן לבצע מעבר על לפי סדר ה
- המשך Map יש הבדל משמעותי בין find() לאופרטור [] - find לא משנה את התוכן של,map בעוד שהאופרטור [] יוצר איבר אם הוא לא נמצא ב.map void f(const map<string, int>& M) { if(m["the meaning"] == 42) { // Error! Cannot use [] on const map objects! if(m.find("the meaning")!= M.end() && M.find("the meaning")->second == 42) { // Correct cout << "Don't Panic!" << endl; כשעובדים עם set ו- map, אין לשנות את הערך של האיברים באמצעות האיטרטורים!
שימוש באובייקטים שונים ב map/set שוב, לפי אותו כלל: השוואות מבוצעות באמצעות אופרטור >. const double epsilon = 1e-7; struct point { double x, y; // Declare operator < taking precision into account bool operator < (const point& p) const { if(x < p.x - epsilon) return true; if(x > p.x + epsilon) return false; if(y < p.y - epsilon) return true; if(y > p.y + epsilon) return false; return false; ; כעת ניתן ליצור set<point> או.map<point,string>
hash_set / hash_map hash_set, hash_map יעילים כאשר אנו מעוניינים לבדוק האם איבר נמצא ב container )למימוש,)dictionaries אך אין בהם חשיבות לסדר. char*, hash ב- STL.int ישנן פונקציות לחלק מהטיפוסים:
- המשך hash_set / hash_map hash_set<int> a_hash_set(size); hash_map<int, int> a_hash_map(size); hash_set<element, hash_elem, eq_elem> elem_hash_set(size); hash_map<element, int, hash_elem, eq_elem> elem_hash_map(size); struct element { int kuku; int kaka; char str[char_arr_size]; ; struct hash_elem { hash<int> h; size_t operator()(const element& e) const { return (h.operator ()(e.kaka)<<16)+h.operator ()(e.kuku); ; struct eq_elem { bool operator()(const element& e1, const element& e2) const { for(int i = 0; i < CHAR_ARR_SIZE; i++) if (e1.str[i]!= e2.str[i]) return false; return e1.kuku == e2.kuku && (e1.kuku == e2.kuku); ;
String Streams istringstream, void f(const string& s) { // Construct an object to parse strings istringstream is(s); לעיתים יש צורך לעבד.strings ++C מספקת שני אובייקטים לכך:.ostringstream // Vector to store data vector<int> v; // Read integer while possible and add it to the vector int tmp; while(is >> tmp) { v.push_back(tmp);
- המשך String Streams string f(const vector<int>& v) { // Construct an object to do formatted output ostringstream os; // Copy all elements from vector<int> to string stream as text tr(v, it) { os << ' ' << *it; // Get string from string stream string s = os.str(); // Remove first space character if(!s.empty()) { // Beware of empty string here s = s.substr(1); return s;
DFS ברי הגעה מ- s. שהם אלגוריתם לגילוי כל הצמתים ב- V בנוסף מאפשר למצוא האם יש מעגלים בעץ, טופולוגי, למצוא רכיבים קשירים היטב. לבצע מיון הרעיון: בכל שלב האלגוריתם מנסה להתקדם לעומק - כאשר נבקר בצומת v, אם יש קשת (v,u) לצומת u שעוד לא נתגלתה נבקר בה ונמשיך את החיפוש ממנה.
DFS using STL typedef vector<int> vi; typedef vector<vi> vvi; int N; // number of vertices vvi W; // graph vi V; // V is a visited flag void dfs(int i) { if(!v[i]) { V[i] = true; for_each(all(w[i]), dfs); bool check_graph_connected_dfs() { int start_vertex = 0; V = vi(n, false); dfs(start_vertex); return (find(all(v), 0) == V.end());
BFS ברי הגעה מ- s. שהם אלגוריתם לגילוי כל הצמתים ב- V בנוסף מאפשר למצוא את המרחק הקצר ביותר )בקשתות( מ- s לכל צומת ב- V. הרעיון: בכל איטרציה האלגוריתם בוחן חזית של צמתים במרחק i מ- s. יש לגלות את כל הצמתים במרחק i מ- s לפני גילוי צומת במרחק 1+i.
BFS using STL int N; // number of vertices vvi W; // lists of adjacent vertices bool check_graph_connected_bfs() { int start_vertex = 0; vi V(N, false); queue<int> Q; Q.push(start_vertex); V[start_vertex] = true; while(!q.empty()) { int i = Q.front(); // get the tail element from queue Q.pop(); tr(w[i], it) { if(!v[*it]) { V[*it] = true; Q.push(*it); return (find(all(v), 0) == V.end());
Dijkstra.s קלט: גרף ממושקל מכוון ללא קשתות שליליות וצומת.d[v] = dist(s,v) פלט: v לכל צומת
Dijkstra pseudo code /* Initialization: set every distance to INFINITY until we discover a path */ for i = 0 to V - 1 end dist[i] = INFINITY prev[i] = NULL /* The distance from the source to the source is defined to be zero */ dist[s] = 0 /* This loop corresponds to sending out the explorers walking the paths, where * the step of picking "the vertex, v, with the shortest path to s" corresponds * to an explorer arriving at an unexplored vertex */ for each edge of v, (v1, v2) /* The next step is sometimes given the confusing name "relaxation" if(dist[v1] + length(v1, v2) < dist[v2]) dist[v2] = dist[v1] + length(v1, v2) prev[v2] = v1 possibly update U, depending on implementation end if end for end while while(f is missing a vertex) pick the vertex, v, in U with the shortest path to s add v to F
- המשך Dijkstra קל מאוד לממש את האלגוריתם של dijkstra מערך מרחקים בסיבוכיות ) 2.O(V בעזרת עבור גרפים דלילים ניתן לממש את האלגוריתם בסיבוכיות O(ElogV) באמצעות ערימה שתומכת ב decrease key )בעזרת ערימות פיבונצ'י ניתן אף לממש בסיבוכיות.)O(E+VlogV) לצערנו, בערימה של )priority_queue( STL אין פונקציית.decrease key מה נעשה?
Dijkstra using STL vi D(N, 987654321); // distance from start vertex to each vertex priority_queue<ii,vector<ii>, greater<ii> > Q; // priority_queue with reverse comparison operator, // so top() will return the least distance // initialize the start vertex, suppose it s zero D[0] = 0; Q.push(ii(0,0)); // iterate while queue is not empty while(!q.empty()) { // fetch the nearest element ii top = Q.top(); Q.pop(); // v is vertex index, d is the distance int v = top.second, d = top.first; // this check is very important // we analyze each vertex only once // the other occurrences of it on queue (added earlier) // will have greater distance if(d <= D[v]) { // iterate through all outcoming edges from v tr(g[v], it) { int v2 = it->first, cost = it->second; if(d[v2] > D[v] + cost) { // update distance if possible D[v2] = D[v] + cost; // add the vertex to queue Q.push(ii(D[v2], v2));